﻿/////////////////// Microsoft Windows PowerShell SnapIn ///////////////////////
//
// Filename:    MetadataExplorerSnapIn.cs
//
// Included Cmdlet(s): Get-ItemMetadata
//
// Recommended Alias:  Gimme (:^D)
//
// Developer:   Jonathan Tigner
//
// Description: Uses Windows Explorer to retrieve file metadata.
//
//              Based on the prior works of Jim Truher, Keith Hill, and Rick Williamson, 
//              detailed in "MSH: Get Extended Properties of a File" 
//              http://keithhill.spaces.live.com/blog/cns!5A8D2641E0963A97!186.entry.
//
//
// Copyright (c) 2008, Jonathan Tigner
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
///////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Data.Linq;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Text;
using Shell32;

namespace JonathanTigner.PowerShell.Commands
{
    #region SnapIn Definition

    /// <summary>
    /// MetadataExplorerSnapIn Class
    /// </summary>
    [RunInstaller(true)]    
    public class MetadataExplorerSnapIn : PSSnapIn
    {
        /// <summary>
        /// Name Property
        /// </summary>
        public override string Name
        {
            get { return "MetadataExplorer"; }
        }
        /// <summary>
        /// Vendor Property
        /// </summary>
        public override string Vendor
        {
            get { return "Jonathan Tigner"; }
        }
        /// <summary>
        /// VendorResource Property
        /// </summary>
        public override string VendorResource
        {
            get { return "Metadata Explorer snap-in,Jonathan Tigner"; }
        }
        /// <summary>
        /// Description Property
        /// </summary>
        public override string Description
        {
            get { return "This Windows PowerShell snap-in contains cmdlets for processing file system metadata"; }
        }
        /// <summary>
        /// DescriptionResource Property
        /// </summary>
        public override string DescriptionResource
        {
            get { return "Metadata Explorer snap-in,This Windows PowerShell snap-in contains cmdlets for processing file system metadata"; }
        }
    }
#endregion SnapIn Definition

    /// <summary>
    /// Get-ItemMetadata Cmdlet
    /// <remarks>
    /// The Get-ItemMetadata Cmdlet retrieves extended file system item metadata via the Windows Explorer shell API.
    /// </remarks>
    /// </summary>
    /// <example>File system path input as a parameter:
    /// <code>
    /// Get-ItemMetadata C:\BattlestarGalactica.dvr.ms
    /// </code>
    /// Metadata retrival from file system items passed through te pipeline:
    /// <code>
    /// Get-ChildItem | Get-ItemMetadata | Format-List *
    /// </code>
    /// </example>
    [Cmdlet(VerbsCommon.Get, "ItemMetadata")]
    public class GetItemMetadataCommand : PSCmdlet
    {
        #region Parameters

        //Unprocessed path input
        private string[] path;

        /// <summary>
        /// File system path parameter.
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
        [ValidateNotNullOrEmpty]
        [Alias("PSPath","Root")]
        public string[] Path
        {
            get { return path; }
            set { path = value; }
        }

        #endregion Parameters

        #region Helper Methods

        /// <summary>
        /// IsFile Method
        /// </summary>
        /// <remarks>
        /// Determines whether a given FileSystemInfo object represents a file.
        /// </remarks>
        /// <param name="fsi">FileSystemInfo (eg., FileInfo or DirectoryInfo) object.</param>
        /// <returns>Returns true if input is a file, otherwise false.</returns>
        private bool IsFile(FileSystemInfo fsi)
        {
            if ((fsi.Attributes & FileAttributes.Directory) == 0)
            {
                return true;
            }
            else return false;
        }

        /// <summary>
        /// IsDirectory Method
        /// </summary>
        /// <remarks>
        /// Determines whether a given FileSystemInfo object represents a directory.
        /// </remarks>
        /// <param name="fsi">FileSystemInfo (eg., FileInfo or DirectoryInfo) object.</param>
        /// <returns>Returns true if input is a directory, otherwise false</returns>
        private bool IsDirectory(FileSystemInfo fsi)
        {
            if ((fsi.Attributes & FileAttributes.Directory) != 0)
            {
                return true;
            }
            else return false;
        }
        
        //Create instance of Windows Explorer shell
        Shell shell = new Shell();


        /// <summary>
        /// InitializeProperties Method
        /// </summary>
        /// <remarks>
        /// Generates file system metadata dictionary.
        /// </remarks>
        /// <returns>
        /// Returns a <c>Dictionary&lt;string, int&gt;</c> containing all available file system properties in Windows.
        /// </returns>        
        private Dictionary<string, int> InitializeProperties()
        {
            var current = Directory.GetCurrentDirectory();
            var directory = shell.NameSpace(current);
            var file = directory.Items().Item(Directory.GetFiles(current)[0]);
            var props = new Dictionary<string, int>();

            for (int i = 0; i < int.MaxValue; i++)
            {

                var p = directory.GetDetailsOf(file, i);

                if (!String.IsNullOrEmpty(p))
                {
                    props.Add(p, i);
                }
                else break;
            }
            return props;
        }

        #endregion Helper Methods


        //Create Dictionary to cache results of InitializeProperties().
        Dictionary<string, int> itemProperties = new Dictionary<string, int>();

        /// <summary>
        /// BeginProcessing Method
        /// </summary>
        protected override void BeginProcessing()
        {
            WriteVerbose("Initializing Properties: ");

            itemProperties = InitializeProperties();
            
#if DEBUG
           WriteDebug("Available Properties: ");

            int i = 0;
            foreach (var s in itemProperties.Keys)
            {
                itemProperties.TryGetValue(s, out i);
                WriteDebug(i + ": " + s);
            }

            WriteDebug("Total Properties: " + itemProperties.Count);
#endif
        }

        /// <summary>
        /// ProcessRecord Method
        /// </summary>
        protected override void ProcessRecord()
        {


            WriteDebug("Paths: ");

            foreach (var psPath in path)
            {
                foreach (var pInfo in SessionState.Path.GetResolvedPSPathFromPSPath(psPath))
                {                    
                    var item = pInfo.ProviderPath;

                    WriteDebug("PathInfo: " + pInfo);
                    WriteDebug(item);

                    var fsi = new DirectoryInfo(item);

                    if (fsi != null)
                    {
                        //Check if item is a directory
                        if (IsDirectory(fsi) && fsi.Exists)
                        {
                            WriteDebug("Item is a directory ");

                            FolderItem fsItem;
                            Folder directory;

                            //Check if path is a root location
                            if (fsi.Parent == null)
                            {
                                //Set Explorer shell namespace to root directory
                                directory = shell.NameSpace(fsi.FullName);

                                //Set Explorer shell namespace to Computer namespace
                                directory = directory.ParentFolder;

                                //Get file system item for given path
                                fsItem = directory.Items().Item(fsi.Name);
                            }
                            else
                            {
                                //Set Explorer shell namespace to parent directory
                                directory = shell.NameSpace(fsi.Parent.FullName);

                                //Get file system item for given path
                                fsItem = directory.Items().Item(fsi.Name);
                            }

                            //Create PSObject
                            var psObj = new PSObject(fsi);

                            //Add custom type information
                            psObj.TypeNames[0] = "Explorer.IO.File.Metadata";

                            //Add extended metadata
                            //Stores associated value of current key
                            int i = 0;
                            foreach (var key in itemProperties.Keys)
                            {
                                //Get integer value of current key and set i to that value
                                itemProperties.TryGetValue(key, out i);

                                //Get metadata for current file system item property
                                var itemProperty = directory.GetDetailsOf(fsItem, itemProperties[key]);

                                //Add NoteProperty corresponding to each metadata item
                                if (psObj.Members.Match(key).Count == 0 && !String.IsNullOrEmpty(itemProperty))
                                {
                                    WriteDebug("Adding NoteProperty: " + key + " = " + itemProperty + "");
                                    psObj.Members.Add(new PSNoteProperty(key, itemProperty));
                                }
                            }

                            //psObj.Members.Add(new PSNoteProperty("Path", fsi.FullName));

                            psObj.Members.Add(new PSNoteProperty("PSPath", pInfo.Path));
                            psObj.Members.Add(new PSNoteProperty("PSProvider", pInfo.Provider));


                            psObj.Members.Add(new PSNoteProperty("PSChildName", fsi.Name));
                            psObj.Members.Add(new PSNoteProperty("PSIsContainer", "True"));

                            
                            WriteVerbose("Writing item metadata: ");

                            WriteObject(psObj);
                        }
                        else if (IsFile(fsi))
                        {
                            var file = new FileInfo(item);
                            if (file.Exists)
                            {
                                WriteDebug("Item is a file ");

                                //Set Explorer shell namespace to directory
                                var directory = shell.NameSpace(file.DirectoryName);

                                //Get file system item for given path
                                var fsItem = directory.Items().Item(file.Name);

                                //Create PSObject
                                var psObj = new PSObject(file);

                                //Add custom type information
                                psObj.TypeNames[0] = "Explorer.IO.File.Metadata";

                                //Add extended metadata
                                int i = 0;
                                foreach (var key in itemProperties.Keys)
                                {
                                    //Get integer value of current key and set i to that value
                                    itemProperties.TryGetValue(key, out i);

                                    //Get metadata for current file system item property
                                    var itemProperty = directory.GetDetailsOf(fsItem, itemProperties[key]);

                                    //Add NoteProperty corresponding to each metadata item
                                    if (psObj.Members.Match(key).Count == 0 && !String.IsNullOrEmpty(itemProperty))
                                    {
                                        WriteDebug("Adding NoteProperty: " + key + " = " + itemProperty);
                                        psObj.Members.Add(new PSNoteProperty(key, itemProperty));
                                    }
                                }
                                
                                psObj.Members.Add(new PSNoteProperty("PSPath", pInfo.Path));
                                psObj.Members.Add(new PSNoteProperty("PSProvider", pInfo.Provider));
                                psObj.Members.Add(new PSNoteProperty("PSChildName", file.Name));
                                psObj.Members.Add(new PSNoteProperty("PSIsContainer", "False"));

                                //var psMemberInfo = new PSMemberInfo();
                                //psMemberInfo.
                                //var psMemberSet = new PSMemberSet(


                                WriteDebug("");
                                WriteVerbose("Writing item metadata: ");
                                WriteObject(psObj);
                            }
                            else
                            {
                                WriteError(new ErrorRecord(new ArgumentException("Cannot find filesystem path because it does not exist", item), "EFP000", ErrorCategory.InvalidArgument, Path));
                            }
                        }
                    }

                }
            }

        }

        /// <summary>
        /// EndProcessing Method
        /// </summary>
        protected override void EndProcessing()
        {
            base.EndProcessing();
        }
    }
}
